package ua.snuk182.asia.services.mrim.inner;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import ua.snuk182.asia.services.ServiceUtils;
import ua.snuk182.asia.services.api.ProtocolUtils;
import ua.snuk182.asia.services.mrim.MrimEntityAdapter;
import ua.snuk182.asia.services.mrim.inner.dataentity.MrimFileTransfer;
import ua.snuk182.asia.services.mrim.inner.dataentity.MrimIncomingFile;
import ua.snuk182.asia.services.mrim.inner.dataentity.MrimPacket;
import android.os.Environment;

public class FileTransferEngine {

	private static final String MRA_FT_HELLO = "MRA_FT_HELLO ";
	private static final String MRA_GET_FILE = "MRA_FT_GET_FILE ";
	private static final String LIST_DATA_DIVIDER = ";";
	private static final String IN_DATA_DIVIDER = ":";

	private static final int SERVER_SOCKET_TIMEOUT = 600000;

	private static final String MrimProxyName = "mrim10-3.mail.ru";

	private byte[] localIp = new byte[] { 0, 0, 0, 0 };

	private final MrimServiceInternal service;
	private final List<MrimFileTransfer> transfers = new ArrayList<MrimFileTransfer>();
	private final Map<Long, FileRunnableService> activeTransfers = new HashMap<Long, FileRunnableService>();

	private List<NotificationData> notifications = new LinkedList<NotificationData>();

	public FileTransferEngine(MrimServiceInternal service) {
		this.service = service;
	}

	public long sendFiles(String buddyMrid, final List<File> files, byte[] localIp) {
		final MrimFileTransfer transfer = getFileTransferRequest(buddyMrid, files);
		this.localIp = localIp;

		transfers.add(transfer);
		new Thread("File sender " + buddyMrid) {

			@Override
			public void run() {
				try {
					sendFileTransferRequest(createPeer(transfer));
				} catch (IOException e) {
					// todo ask mirror?
				}
			}

		}.start();
		return transfer.messageId;
	}

	private FileRunnableService createPeer(MrimFileTransfer transfer) throws IOException {
		service.log("ft: creating own peer");
		FileRunnableService frs = activeTransfers.get(transfer.messageId);

		if (frs == null) {
			service.log("ft: new runnable for " + transfer.buddyMrid);
			frs = new FileRunnableService(transfer);
			frs.connectionState = FileRunnableService.CONNSTATE_HANDSHAKE;
			activeTransfers.put((long) transfer.messageId, frs);
		} else {
			service.log("ft: existing runnable for " + transfer.buddyMrid);
			frs.transfer = transfer;
		}
		frs.connectionState = FileRunnableService.CONNSTATE_HANDSHAKE;

		frs.server = createLocalSocket(frs);
		/*
		 * message.externalPort = frs.server.getLocalPort(); message.rvIp =
		 * ProtocolUtils.getIPString(service.getInternalIp());
		 * message.rvMessageType = 0;
		 */
		return frs;
	}

	private void sendFileTransferRequest(FileRunnableService frs) {

		String myIpPortStr = ProtocolUtils.getIPString(localIp) + IN_DATA_DIVIDER + frs.server.getLocalPort() + LIST_DATA_DIVIDER;

		service.log("--- my ipport " + myIpPortStr);

		MrimPacket packet = new MrimPacket();
		packet.type = MrimConstants.MRIM_CS_FILE_TRANSFER;
		byte[] to = MrimEntityAdapter.string2lpsa(frs.transfer.buddyMrid);
		byte[] sessionId = ProtocolUtils.int2ByteLE(frs.transfer.messageId);

		long ln = 0;

		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < frs.transfer.files.size(); i++) {
			File file = frs.transfer.files.get(i);
			ln += file.length();

			sb.append(file.getName().replaceAll(IN_DATA_DIVIDER, "_").replaceAll(LIST_DATA_DIVIDER, "_"));
			// sb.append(IN_DATA_DIVIDER);
			sb.append(LIST_DATA_DIVIDER);
			sb.append(file.length());
			sb.append(LIST_DATA_DIVIDER);
		}
		byte[] lengthSum = ProtocolUtils.long2ByteLE(ln);
		byte[] filenames = MrimEntityAdapter.string2lpsa(sb.toString());
		byte[] unk1 = MrimEntityAdapter.string2lpsa("");
		byte[] myIpPort = MrimEntityAdapter.string2lpsa(myIpPortStr);

		byte[] internalData = new byte[to.length + sessionId.length + 4 + 4 + filenames.length + unk1.length + myIpPort.length];

		int i = 0;
		System.arraycopy(to, 0, internalData, i, to.length);
		i += to.length;
		System.arraycopy(sessionId, 0, internalData, i, sessionId.length);
		i += sessionId.length;
		System.arraycopy(lengthSum, 0, internalData, i, 4);
		i += 4;
		System.arraycopy(ProtocolUtils.int2ByteLE(filenames.length + unk1.length + myIpPort.length), 0, internalData, i, 4);
		i += 4;
		System.arraycopy(filenames, 0, internalData, i, filenames.length);
		i += filenames.length;
		System.arraycopy(unk1, 0, internalData, i, unk1.length);
		i += unk1.length;
		System.arraycopy(myIpPort, 0, internalData, i, myIpPort.length);
		i += myIpPort.length;

		packet.rawData = internalData;

		service.getRunnableService().sendToSocket(packet);
	}

	private byte[] getHandshakeData(MrimFileTransfer transfer) {
		service.log("get handshake for " + transfer.host + " id " + transfer.messageId);

		String str = new String(MRA_FT_HELLO + service.getMrid());
		byte[] boo = new byte[str.length() + 1];
		System.arraycopy(str.getBytes(), 0, boo, 0, str.length());
		boo[boo.length - 1] = 0;
		return boo;
	}

	private ServerSocket createLocalSocket(final FileRunnableService frs) throws IOException {
		final ServerSocket server = new ServerSocket(0);

		new Thread() {

			@Override
			public void run() {
				try {
					setName("FT Server socket listener " + server.getLocalPort());
					server.setSoTimeout(SERVER_SOCKET_TIMEOUT);
					Socket socket = server.accept();

					frs.socket = socket;
					service.log("client connected");
					frs.start();
				} catch (Exception e) {
					service.log(e);
				}
			}

		}.start();

		return server;
	}

	private MrimFileTransfer getFileTransferRequest(String buddyMrid, List<File> files) {
		MrimFileTransfer transfer = new MrimFileTransfer();
		transfer.buddyMrid = buddyMrid;
		transfer.files = files;
		transfer.messageId = files.hashCode() + new Random().nextInt();
		return transfer;
	}

	class FileRunnableService extends Thread {
		public static final int CONNSTATE_CONNECTED = 0;
		public static final int CONNSTATE_HANDSHAKE = 1;
		public static final int CONNSTATE_FILE_HEADER = 2;
		public static final int CONNSTATE_FILE_BODY = 3;
		public static final int CONNSTATE_FILE_SENT = 4;
		public static final int CONNSTATE_DISCONNECTED = 5;

		ServerSocket server = null;
		Socket socket;
		int connectionState = CONNSTATE_CONNECTED;
		MrimFileTransfer transfer;
		String participantUid = null;
		List<byte[]> blobs = new LinkedList<byte[]>();

		long currentFileSizeLeft = 0;
		long currentFileSize = 0;
		byte[] currentFileInfo = null;
		private ExtendedBufferedOutputStream currentFileStream;

		byte[] buffer = null;

		public FileRunnableService(MrimFileTransfer transfer) {
			this(null, transfer);
		}

		public FileRunnableService(Socket socket, MrimFileTransfer transfer) {
			this.socket = socket;
			this.transfer = transfer;
			if (transfer.files != null) {
				currentFileSize = 0;
				for (File f : transfer.files) {
					currentFileSize += f.length();
				}
			}

			participantUid = transfer.buddyMrid;

			setName("File transfer " + transfer.buddyMrid + " " + transfer.messageId);
		}

		@Override
		public void run() {
			if (socket == null) {
				return;
			}

			getDataFromSocket();
		}

		private ExtendedBufferedOutputStream createFile(String filename, long filesize, long modTime, MrimFileTransfer transfer, String participantUid) {
			// Dummy
			String storageState = Environment.getExternalStorageState();
			if (storageState.equals(Environment.MEDIA_MOUNTED)) {
				try {
					File file = ServiceUtils.createLocalFileForReceiving(filename, filesize, modTime);
					FileOutputStream fos = new FileOutputStream(file, true);
					ExtendedBufferedOutputStream os = new ExtendedBufferedOutputStream(file, fos);
					return os;
				} catch (IOException e) {
					e.printStackTrace();
				}
			} else {
				transferFailed(transfer, "No storage mounted");
			}
			return null;
		}

		private void sendFileRequest(MrimIncomingFile file) {
			currentFileStream = createFile(file.filename, file.filesize, new Date().getTime(), transfer, participantUid);

			byte[] infoBlob;

			String str = MRA_GET_FILE + file.filename;
			infoBlob = new byte[str.length() + 1];
			System.arraycopy(str.getBytes(), 0, infoBlob, 0, infoBlob.length - 1);
			infoBlob[infoBlob.length - 1] = 0;

			currentFileSize = file.filesize;
			currentFileSizeLeft = file.filesize;
			connectionState = CONNSTATE_FILE_BODY;
			sendToSocket(infoBlob);
		}

		private void sendProxyHandshake() {
			connectionState = CONNSTATE_CONNECTED;
			try {
				sendToSocket(getProxyHandshakeData(transfer));
			} catch (MrimException e) {
				service.log(e);
				transferFailed(transfer, e.getLocalizedMessage());
				notifyFail(transfer);
				cleanup();
			}
		}

		private void sendHandshake() {
			connectionState = CONNSTATE_HANDSHAKE;
			sendToSocket(getHandshakeData(transfer));
		}

		public byte[] getProxyHandshakeData(MrimFileTransfer transfer) throws MrimException {
			MrimPacket packet = new MrimPacket();
			packet.type = MrimConstants.MRIM_CS_PROXY_HELLO;
			packet.rawData = transfer.proxySessionId;
			return service.processor.packet2Bytes(packet);
		}

		private void getDataFromSocket() {
			int read = 0;
			boolean fullPacket = true;
			final List<byte[]> tmpBlobs = new LinkedList<byte[]>();

			while (connectionState != CONNSTATE_DISCONNECTED && socket != null && socket.isConnected() && !socket.isClosed()) {
				InputStream is;
				try {
					is = socket.getInputStream();

					if (is.available() < 1) {
						Thread.sleep(300);
					} else {
						Thread.sleep(500);

						switch (connectionState) {
						case CONNSTATE_CONNECTED:
							byte[] head = new byte[44];

							is.read(head, 0, 44);

							service.log("-- FT got " + ProtocolUtils.getSpacedHexString(head));

							int ack = ProtocolUtils.bytes2IntLE(head, 12);

							if (ack == MrimConstants.MRIM_CS_PROXY_HELLO_ACK) {
								connectionState = CONNSTATE_HANDSHAKE;
								if (transfer.files == null) {
									sendHandshake();
								}
							}

							break;
						case CONNSTATE_HANDSHAKE:
						case CONNSTATE_FILE_HEADER:
							read = 0;
							byte[] blob = new byte[is.available()];
							is.read(blob, 0, blob.length);

							String str = new String(blob);
							service.log("-- FT got " + ProtocolUtils.getSpacedHexString(blob) + " (" + str + ")");

							connectionState = CONNSTATE_FILE_HEADER;
							if (str.contains(MRA_FT_HELLO)) {
								if (transfer.files != null) { // i am sender
									if (transfer.connection != MrimFileTransfer.CONN_MIRROR) {
										sendHandshake();
									}
								} else {
									if (transfer.connection == MrimFileTransfer.CONN_MIRROR) {
										sendHandshake();
									}
									// service.getRunnableService().sendToSocket(getAcceptMessage(transfer));
									sendFileRequest(transfer.incomingFiles.remove(0));
								}
								break;
							}

							if (str.contains(MRA_GET_FILE)) {
								processHeader(blob);
							}
							break;
						case CONNSTATE_FILE_BODY:
							if (buffer == null) {
								buffer = new byte[88000];
							}

							read = is.read(buffer, 0, buffer.length);
							service.log("read " + read + "| bytes left " + currentFileSizeLeft);
							currentFileSizeLeft -= read;
							fileData(buffer, read, currentFileSizeLeft);

							if (currentFileSizeLeft < 1) {
								// sendFileAck();
								connectionState = CONNSTATE_FILE_HEADER;
								buffer = null;

								if (transfer.incomingFiles != null) {
									if (transfer.incomingFiles.size() > 0) {
										sendFileRequest(transfer.incomingFiles.remove(0));
									} else {
										cleanup();
									}
								}

								if (transfer.files != null && transfer.files.size() < 1) {
									cleanup();
								}
							}
							continue;
						}

						if (fullPacket) {
							byte[] boo = new byte[0];
							for (byte[] i : tmpBlobs) {
								boo = ProtocolUtils.concatByteArrays(boo, i);
							}

							tmpBlobs.clear();
							blobs.add(boo);

						}
					}
				} catch (IOException e) {
					service.log(e);
				} catch (InterruptedException e) {
					service.log(e);
				}
			}
			cleanup();
		}

		private synchronized void fileData(byte[] blob, int read, final long bytesLeft) {
			if (currentFileStream != null) {
				try {
					/*
					 * if (connectionState == CONNSTATE_FILE_HEADER){
					 * currentFileStream.close(); currentFileStream = null; }
					 * else if (connectionState == CONNSTATE_FILE_BODY){
					 * currentFileStream.write(blob); }
					 */

					if (connectionState == CONNSTATE_FILE_BODY) {
						currentFileStream.write(blob, 0, read);
						currentFileStream.flush();

						if (bytesLeft < 1) {
							connectionState = CONNSTATE_FILE_HEADER;
							final String filename = currentFileStream.file.getAbsolutePath();
							currentFileStream.close();
							currentFileStream = null;
							service.log(filename + " got");

							sendNotification(transfer.messageId, filename, currentFileSize, currentFileSize - bytesLeft, true, null, participantUid);
						} else {
							sendNotification(transfer.messageId, currentFileStream.getFile().getAbsolutePath(), currentFileSize, currentFileSize - bytesLeft, true, null, participantUid);
						}
					}
					// messageId, filename, totalSize, sizeTransferred,
					// isReceive, error

				} catch (IOException e) {
					ServiceUtils.log(e);
					try {
						currentFileStream.close();
					} catch (IOException e1) {
						ServiceUtils.log(e);
					}
					currentFileStream = null;
				}
			}
		}

		private void cleanup() {
			try {
				transfers.remove(transfer);
				activeTransfers.remove(transfer.messageId);
				if (server != null) {
					server.close();
					server = null;
				}
				socket.close();
			} catch (IOException e) {
				service.log(e);
			}
		}

		public synchronized boolean sendToSocket(byte[] out) {
			try {
				OutputStream os = socket.getOutputStream();

				service.log("-- FT To be sent " + ProtocolUtils.getSpacedHexString(out) + "/" + new String(out));
				os.write(out);

			} catch (IOException e) {
				connectionState = CONNSTATE_DISCONNECTED;
				service.log(e);
			}
			return true;
		}

		private void processHeader(byte[] blob) {
			if (blob.length < 1) {
				return;
			}
			String str = ProtocolUtils.getEncodedString(blob, 0, blob.length - 1);

			int index = str.indexOf(MRA_GET_FILE);
			if (index < 0) {
				return;
			}

			service.log("got header");
			currentFileInfo = blob;

			try {
				String fileInfo = str.substring(index + MRA_GET_FILE.length());
				service.log(transfer.buddyMrid + " asks for file " + fileInfo);

				for (int i = 0; i < transfer.files.size(); i++) {
					File fi = transfer.files.get(i);
					if (fi.getName().replaceAll(IN_DATA_DIVIDER, "_").replaceAll(LIST_DATA_DIVIDER, "_").equals(fileInfo)) {
						sendFileToSocket(fi);
						transfer.files.remove(fi);
						if (transfer.files.size() < 1) {
							cleanup();
						}
						return;
					}
				}
				transferFailed(transfer, "unknown file");

			} catch (Exception e) {
				service.log(e);
				transferFailed(transfer, e.getLocalizedMessage());
			}
			cleanup();
			notifyFail(transfer);
		}

		private void sendFileToSocket(final File file) {
			OutputStream os;
			try {
				os = socket.getOutputStream();
			} catch (IOException e) {
				service.log(e);
				transferFailed(transfer, e.getLocalizedMessage());
				notifyFail(transfer);
				cleanup();
				return;
			}
			long length = file.length();
			if (length > 8000) {
				buffer = new byte[8000];
			} else {
				buffer = new byte[(int) length];
			}

			currentFileSizeLeft = 0;
			int read = 0;
			service.log("sending " + file.getName() + " to " + participantUid);

			FileInputStream fis;
			try {
				fis = new FileInputStream(file);
				BufferedInputStream bis = new BufferedInputStream(fis, 8000);
				while (currentFileSizeLeft < length) {
					read = bis.read(buffer, 0, buffer.length);
					if (read < 0) {
						break;
					}
					os.write(buffer, 0, read);
					os.flush();
					currentFileSizeLeft += read;
					service.log("sent " + currentFileSizeLeft + " bytes");

					sendNotification(transfer.messageId, file.getAbsolutePath(), length, currentFileSizeLeft, false, null, participantUid);

					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						service.log(e);
					}
				}
			} catch (IOException e) {
				service.log(e);
				transferFailed(transfer, e.getLocalizedMessage());
				notifyFail(transfer);
				cleanup();
				return;
			}
			connectionState = CONNSTATE_FILE_HEADER;

			service.log(file.getName() + " sent");
		}

		private class ExtendedBufferedOutputStream extends BufferedOutputStream {

			private final File file;

			public ExtendedBufferedOutputStream(File file, OutputStream os) {
				super(os, 88000);
				this.file = file;
			}

			public File getFile() {
				return file;
			}
		}
	}

	public void parseFTRequest(MrimPacket packet) {
		MrimFileTransfer transfer = new MrimFileTransfer();

		int version = ProtocolUtils.bytes2IntLE(packet.rawData, 4);

		if (version > MrimConstants.PROTO_VERSION) {
			int i = 44;
			transfer.buddyMrid = MrimEntityAdapter.lpsa2String(packet.rawData, i);
			i += 4 + transfer.buddyMrid.length();
			transfer.messageId = ProtocolUtils.bytes2IntLE(packet.rawData, i);
			i += 4;
			i += 4; // total filesize - useless
			i += 4; // total file info size - useless

			String fileStr = MrimEntityAdapter.lpsa2String(packet.rawData, i);

			parseFiles(fileStr, transfer);

			i += 4 + fileStr.length();

			int nextPacket = ProtocolUtils.bytes2IntLE(packet.rawData, i);
			i += 4;
			if (nextPacket != 0) {
				int count = ProtocolUtils.bytes2IntLE(packet.rawData, i);
				i += 4;
				for (int ii = 0; ii < count; ii++) {
					fileStr = MrimEntityAdapter.lpsw2String(packet.rawData, i);
					i += 4 + 2 * fileStr.length();

					parseFiles(fileStr, transfer);
				}
			}

			String ipData = MrimEntityAdapter.lpsa2String(packet.rawData, i);
			parseIPString(transfer, ipData);

			transfers.add(transfer);
		} else {
			int i = 44;
			transfer.buddyMrid = MrimEntityAdapter.lpsa2String(packet.rawData, i);
			i += 4 + transfer.buddyMrid.length();
			transfer.messageId = ProtocolUtils.bytes2IntLE(packet.rawData, i);
			i += 4;
			i += 4; // total filesize - useless
			i += 4; // total file info size - useless

			String fileStr = MrimEntityAdapter.lpsa2String(packet.rawData, i);

			String[] strFiles = fileStr.split(LIST_DATA_DIVIDER);
			transfer.incomingFiles = new LinkedList<MrimIncomingFile>();
			for (String file : strFiles) {
				if (file.length() < 1) {
					continue;
				}

				String[] attrs = file.split(IN_DATA_DIVIDER);
				MrimIncomingFile ifile = new MrimIncomingFile();
				ifile.filename = attrs[0];
				ifile.filesize = Long.parseLong(attrs[1]);
				transfer.incomingFiles.add(ifile);
			}

			i += 4 + fileStr.length();

			i += 4; // data divider

			String ipData = MrimEntityAdapter.lpsa2String(packet.rawData, i);
			parseIPString(transfer, ipData);

			transfers.add(transfer);
		}

		service.getServiceResponse().respond(MrimServiceResponse.RES_FILEMESSAGE, transfer);
	}

	private void parseFiles(String fileStr, MrimFileTransfer transfer) {
		String[] strFiles = fileStr.split(LIST_DATA_DIVIDER);
		transfer.incomingFiles = new LinkedList<MrimIncomingFile>();

		for (int ii = 0; ii < strFiles.length;) {
			MrimIncomingFile ifile = new MrimIncomingFile();
			ifile.filename = strFiles[ii];
			ii++;
			ifile.filesize = Long.parseLong(strFiles[ii]);
			transfer.incomingFiles.add(ifile);
			ii++;
		}
	}

	public void parseFTResponse(MrimPacket packet) {
		int i = 44;
		int status = ProtocolUtils.bytes2IntLE(packet.rawData, i);
		i += 4;
		String from = MrimEntityAdapter.lpsa2String(packet.rawData, i);
		i += from.length() + 4;
		int msgId = ProtocolUtils.bytes2IntLE(packet.rawData, i);
		i += 4;

		service.log("Ft response " + status + " from " + from + " for " + msgId);
		MrimFileTransfer transfer = findTransfer(msgId);

		if (transfer == null)
			return;

		switch (status) {
		case MrimConstants.FILE_TRANSFER_STATUS_OK:
			break;
		case MrimConstants.FILE_TRANSFER_STATUS_DECLINE: // here so far
			transferFailed(transfer, "Cancelled");
			break;
		case MrimConstants.FILE_TRANSFER_STATUS_INCOMPATIBLE_VERS:
		case MrimConstants.FILE_TRANSFER_STATUS_ERROR:
			transferFailed(transfer, "remote error");
			break;
		case MrimConstants.FILE_TRANSFER_MIRROR:
			String ipData = MrimEntityAdapter.lpsa2String(packet.rawData, i);
			transfer.connection = MrimFileTransfer.CONN_MIRROR;
			parseIPString(transfer, ipData);
			connectPeer(transfer, null);
			break;
		}
	}

	private void parseIPString(MrimFileTransfer transfer, String ipData) {
		String[] ipDataParts = ipData.split(LIST_DATA_DIVIDER);
		for (String data : ipDataParts) {
			if (data.length() < 1) {
				continue;
			}

			String[] connection = data.split(IN_DATA_DIVIDER);

			transfer.host = connection[0];
			transfer.port = Integer.parseInt(connection[1]);

			if (transfer.port != 443) {// TODO do we need ssl? do we need more
										// ips?
				break;
			}
		}
	}

	private MrimFileTransfer findTransfer(int msgId) {
		for (MrimFileTransfer tr : transfers) {
			if (tr.messageId == msgId) {
				return tr;
			}
		}
		return null;
	}

	private void connectPeer(MrimFileTransfer transfer, FileRunnableService runnable) {
		service.log("connecting peer " + transfer.host + ":" + transfer.port + " for " + transfer.messageId + "//receiver ");

		Socket socket;
		try {
			socket = new Socket();
			socket.connect(new InetSocketAddress(InetAddress.getByAddress(ProtocolUtils.ipString2ByteBE(transfer.host)), transfer.port), 10000);
		} catch (Exception e) {
			service.log(e);
			socket = null;
		}

		if (socket != null && socket.isConnected()) {
			service.log("ft: direct socket connected for " + transfer.messageId);
			if (runnable == null) {
				service.log("ft: new runnable for " + transfer.messageId);
				runnable = new FileRunnableService(socket, transfer);
				activeTransfers.put((long) transfer.messageId, runnable);
			} else {
				service.log("ft: existing runnable for " + transfer.messageId);
				if (runnable.server != null) {
					try {
						runnable.server.close();
						runnable.server = null;
					} catch (IOException e) {
						service.log(e);
					}
				}

				if (runnable.socket != null) {
					try {
						runnable.socket.close();
					} catch (IOException e) {
						service.log(e);
					}
				}
				runnable.socket = socket;

			}
			runnable.start();

			if (transfer.connection == MrimFileTransfer.CONN_MIRROR) {
				runnable.sendHandshake();
			}

			if (transfer.connection == MrimFileTransfer.CONN_PROXY) {
				runnable.sendProxyHandshake();
			}

		} else {
			if (runnable == null) {
				runnable = new FileRunnableService(socket, transfer);
			}

			try {
				if (runnable.server != null) {
					runnable.server.close();
					runnable.server = null;
				}
			} catch (IOException e) {
				service.log(e);
			}

			runnable.socket = socket;

			service.log("ft: no direct connection");
			if (transfer.connection == MrimFileTransfer.CONN_PEER && transfer.files == null) {
				createMirror(transfer);
			} else if (transfer.connection == MrimFileTransfer.CONN_MIRROR && transfer.files != null) {
				createProxyCall(transfer);
			} else {
				transferFailed(transfer, "no route to host");
				notifyFail(transfer);
			}
		}
	}

	private void createProxyCall(MrimFileTransfer transfer) {
		try {
			transfer.connection = MrimFileTransfer.CONN_PROXY;
			InetAddress inetAdd = InetAddress.getByName(MrimProxyName);
			transfer.host = inetAdd.getHostAddress();
			transfer.port = 2041;

			service.getRunnableService().sendToSocket(getFTProxyRedirectCall(transfer));
		} catch (Exception uhe) {
			service.log(uhe);
		}
	}

	private MrimPacket getFTProxyRedirectCall(MrimFileTransfer transfer) throws IOException {
		MrimPacket packet = new MrimPacket();
		packet.type = MrimConstants.MRIM_CS_PROXY;

		ByteArrayOutputStream stream = new ByteArrayOutputStream();

		stream.write(MrimEntityAdapter.string2lpsa(transfer.buddyMrid));
		stream.write(ProtocolUtils.int2ByteLE(transfer.messageId));
		stream.write(ProtocolUtils.int2ByteLE(MrimConstants.MRIM_PROXY_TYPE_FILES));

		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < transfer.files.size(); i++) {
			File file = transfer.files.get(i);

			sb.append(file.getName().replaceAll(IN_DATA_DIVIDER, "_").replaceAll(LIST_DATA_DIVIDER, "_"));
			// sb.append(IN_DATA_DIVIDER);
			sb.append(LIST_DATA_DIVIDER);
			sb.append(file.length());
			sb.append(LIST_DATA_DIVIDER);
		}

		stream.write(MrimEntityAdapter.string2lpsa(sb.toString()));
		stream.write(MrimEntityAdapter.string2lpsa(transfer.host + IN_DATA_DIVIDER + transfer.port + LIST_DATA_DIVIDER));
		/*
		 * transfer.proxySessionId = new byte[16];
		 * 
		 * Random random = new Random();
		 * System.arraycopy(ProtocolUtils.long2ByteLE(random.nextLong()), 0,
		 * transfer.proxySessionId, 0, 8);
		 * System.arraycopy(ProtocolUtils.long2ByteLE(random.nextLong()), 0,
		 * transfer.proxySessionId, 8, 8);
		 * stream.write(transfer.proxySessionId);
		 */

		stream.write(new byte[16]);

		packet.rawData = stream.toByteArray();

		return packet;
	}

	private void createMirror(MrimFileTransfer transfer) {
		try {
			MrimPacket packet = getAnswerMessage(transfer, MrimConstants.FILE_TRANSFER_MIRROR);

			FileRunnableService frs = createPeer(transfer);

			String ipData = ProtocolUtils.getIPString(localIp) + IN_DATA_DIVIDER + frs.server.getLocalPort();

			packet.rawData = ProtocolUtils.concatByteArrays(packet.rawData, MrimEntityAdapter.string2lpsa(ipData));

			transfer.connection = MrimFileTransfer.CONN_MIRROR;

			service.getRunnableService().sendToSocket(packet);

		} catch (IOException e) {
			transferFailed(transfer, e.toString());
			notifyFail(transfer);
		}
	}

	private void notifyFail(MrimFileTransfer transfer) {
		service.getRunnableService().sendToSocket(getAnswerMessage(transfer, MrimConstants.FILE_TRANSFER_STATUS_ERROR));
	}

	private void transferFailed(MrimFileTransfer transfer, String error) {
		service.log(error);
		transfers.remove(transfer);
		activeTransfers.remove(transfer.messageId);
		sendNotification(transfer.messageId, transfer.buddyMrid, 0L, 0L, false, error, transfer.buddyMrid);
	}

	@SuppressWarnings("unused")
	private MrimPacket getAcceptMessage(MrimFileTransfer transfer) {
		return getAnswerMessage(transfer, MrimConstants.FILE_TRANSFER_STATUS_OK);
	}

	private MrimPacket getAnswerMessage(MrimFileTransfer transfer, int ftStatus) {
		MrimPacket packet = new MrimPacket();
		packet.type = MrimConstants.MRIM_CS_FILE_TRANSFER_ACK;

		byte[] mrid = MrimEntityAdapter.string2lpsa(transfer.buddyMrid);
		byte[] blob = new byte[8 + mrid.length];

		int i = 0;
		System.arraycopy(ProtocolUtils.int2ByteLE(ftStatus), 0, blob, i, 4);
		i += 4;

		System.arraycopy(mrid, 0, blob, i, mrid.length);
		i += mrid.length;
		System.arraycopy(ProtocolUtils.int2ByteLE(transfer.messageId), 0, blob, i, 4);

		packet.rawData = blob;
		return packet;
	}

	private synchronized void sendNotification(int messageId, String filename, long totalSize, long sizeSent, boolean incoming, String error, String participantUid) {
		NotificationData data = new NotificationData(messageId, filename, totalSize, sizeSent, incoming, error, participantUid);
		notifications.add(data);

		new Thread("Notification") {

			@Override
			public void run() {
				sendNotifications();
			}

		}.start();
	}

	private void sendNotifications() {
		synchronized (notifications) {
			while (notifications.size() > 0) {
				NotificationData data = notifications.remove(0);
				service.getServiceResponse().respond(MrimServiceResponse.RES_FILEPROGRESS, (long) data.messageId, data.filePath, data.totalSize, data.sent, data.incoming, data.error, data.participantUid);
			}
		}
	}

	private class NotificationData {

		public int messageId;
		public String filePath;
		public long totalSize;
		public long sent;
		public boolean incoming;
		public String error;
		public String participantUid;

		public NotificationData(int messageId, String filePath, long totalSize, long sent, boolean incoming, String error, String participantUid) {
			this.messageId = messageId;
			this.filePath = filePath;
			this.totalSize = totalSize;
			this.sent = sent;
			this.incoming = incoming;
			this.error = error;
			this.participantUid = participantUid;
		}
	}

	public void cancelAll() {
		for (FileRunnableService runnable : activeTransfers.values()) {
			if (runnable.server != null && !runnable.server.isClosed()) {
				try {
					runnable.server.close();
				} catch (IOException e) {
					service.log(e);
				}
			}
			if (runnable.socket != null && !runnable.socket.isClosed()) {
				try {
					runnable.socket.close();
				} catch (IOException e) {
					service.log(e);
				}
			}
		}
	}

	public void fileReceiveResponse(Long msgId, Boolean accept, byte[] internalIp) {
		MrimFileTransfer transfer = findTransfer(msgId.intValue());

		if (transfer == null) {
			service.log("ft: no message");
			return;
		}

		this.localIp = internalIp;
		if (!accept) {
			service.log("ft: reject " + transfer.messageId);
			service.getRunnableService().sendToSocket(getAnswerMessage(transfer, MrimConstants.FILE_TRANSFER_STATUS_DECLINE));
		} else {
			service.log("ft: accept " + transfer.messageId);
			connectPeer(transfer, null);
		}
	}

	public void parseFTProxyConnectionRequest(MrimPacket packet) {
		int pos = 44;

		String email = MrimEntityAdapter.lpsa2String(packet.rawData, pos);
		service.log("proxy " + email);

		pos += email.length() + 4;

		int id = ProtocolUtils.bytes2IntLE(packet.rawData, pos);
		pos += 4;

		service.log("proxy id " + id);

		int dataType = ProtocolUtils.bytes2IntLE(packet.rawData, pos);
		pos += 4;

		service.log("proxy type " + dataType);

		if (dataType != MrimConstants.MRIM_PROXY_TYPE_FILES) {
			return;
		}

		MrimFileTransfer transfer = findTransfer(id);
		if (transfer == null) {
			return;
		}

		String rawdata = MrimEntityAdapter.lpsa2String(packet.rawData, pos);
		service.log("proxy data " + rawdata);

		parseFiles(rawdata, transfer);

		pos += rawdata.length() + 4;

		String iplist = MrimEntityAdapter.lpsa2String(packet.rawData, pos);
		service.log("proxy iplist " + iplist);

		pos += iplist.length() + 4;

		transfer.proxySessionId = new byte[16];
		System.arraycopy(packet.rawData, pos, transfer.proxySessionId, 0, 16);
		pos += 16;

		byte[] answerData = new byte[packet.rawData.length - 40];
		System.arraycopy(ProtocolUtils.int2ByteLE(MrimConstants.PROXY_STATUS_OK), 0, answerData, 0, 4);
		System.arraycopy(packet.rawData, 44, answerData, 4, packet.rawData.length - 44);

		MrimPacket ack = new MrimPacket();
		ack.type = MrimConstants.MRIM_CS_PROXY_ACK;
		ack.rawData = answerData;

		service.getRunnableService().sendToSocket(ack);

		transfer.connection = MrimFileTransfer.CONN_PROXY;
		parseIPString(transfer, iplist);
		connectPeer(transfer, activeTransfers.get(id));
	}

	public void parseFTProxyAck(MrimPacket packet) {
		int pos = 44;

		int result = ProtocolUtils.bytes2IntLE(packet.rawData, pos);
		pos += 4;

		String email = MrimEntityAdapter.lpsa2String(packet.rawData, pos);
		service.log("proxy " + email);

		pos += email.length() + 4;

		int id = ProtocolUtils.bytes2IntLE(packet.rawData, pos);
		pos += 4;

		service.log("proxy id " + id);

		int dataType = ProtocolUtils.bytes2IntLE(packet.rawData, pos);
		pos += 4;

		service.log("proxy type " + dataType);

		if (dataType != MrimConstants.MRIM_PROXY_TYPE_FILES) {
			return;
		}

		MrimFileTransfer transfer = findTransfer(id);
		if (transfer == null) {
			return;
		}

		FileRunnableService frs = activeTransfers.get(id);

		service.log("proxy id " + id);
		if (result != MrimConstants.PROXY_STATUS_OK) {
			service.log("Proxy error " + result);
			transferFailed(transfer, "Proxy error");
			notifyFail(transfer);
			if (frs != null) {
				frs.cleanup();
			}
			return;
		}

		if (transfer.proxySessionId != null) {
			service.log("proceed file send " + frs.connectionState);
			return;
		}

		String rawdata = MrimEntityAdapter.lpsa2String(packet.rawData, pos);
		service.log("proxy data " + rawdata);

		parseFiles(rawdata, transfer);

		pos += rawdata.length() + 4;

		String iplist = MrimEntityAdapter.lpsa2String(packet.rawData, pos);
		service.log("proxy iplist " + iplist);

		pos += iplist.length() + 4;

		transfer.proxySessionId = new byte[16];
		System.arraycopy(packet.rawData, pos, transfer.proxySessionId, 0, 16);
		pos += 16;

		parseIPString(transfer, iplist);

		connectPeer(transfer, frs);
	}
}
